export const version = '__MICRO_APP_VERSION__';

// do not use isUndefined
export const isBrowser = typeof window !== 'undefined';

// do not use isUndefined
export const globalThis =
  typeof global !== 'undefined'
    ? global
    : typeof window !== 'undefined'
    ? window
    : typeof self !== 'undefined'
    ? self
    : Function('return this')();

// is Undefined
export function isUndefined(target) {
  return target === undefined;
}

// is Null
export function isNull(target) {
  return target === null;
}

// is String
export function isString(target) {
  return typeof target === 'string';
}

// is Boolean
export function isBoolean(target) {
  return typeof target === 'boolean';
}

// is function
export function isFunction(target) {
  return typeof target === 'function';
}

// is Array
export const isArray = Array.isArray;

// is PlainObject
export function isPlainObject(target) {
  return toString.call(target) === '[object Object]';
}

// is Promise
export function isPromise(target) {
  return toString.call(target) === '[object Promise]';
}

// is bind function
export function isBoundFunction(target) {
  return (
    isFunction(target) && target.name.indexOf('bound ') === 0 && !target.hasOwnProperty('prototype')
  );
}

// is ShadowRoot
export function isShadowRoot(target) {
  return typeof ShadowRoot !== 'undefined' && target instanceof ShadowRoot;
}

export const rawDefineProperty = Object.defineProperty;

export const rawDefineProperties = Object.defineProperties;
export const rawHasOwnProperty = Object.prototype.hasOwnProperty;

/**
 * format error log
 * @param msg message
 * @param appName app name, default is null
 */
export function logError(msg, appName = null, ...rest) {
  const appNameTip = appName && isString(appName) ? ` app ${appName}:` : '';
  if (isString(msg)) {
    console.error(`[micro-app]${appNameTip} ${msg}`, ...rest);
  } else {
    console.error(`[micro-app]${appNameTip}`, msg, ...rest);
  }
}

/**
 * format warn log
 * @param msg message
 * @param appName app name, default is null
 */
export function logWarn(msg, appName = null, ...rest) {
  const appNameTip = appName && isString(appName) ? ` app ${appName}:` : '';
  if (isString(msg)) {
    console.warn(`[micro-app]${appNameTip} ${msg}`, ...rest);
  } else {
    console.warn(`[micro-app]${appNameTip}`, msg, ...rest);
  }
}

/**
 * async execution
 * @param fn callback
 * @param args params
 */
export function defer(fn, ...args) {
  Promise.resolve().then(fn.bind(null, ...args));
}

/**
 * Add address protocol
 * @param url address
 */
export function addProtocol(url) {
  return url.startsWith('//') ? `${location.protocol}${url}` : url;
}

/**
 * format URL address
 * note the scenes:
 * 1. micro-app -> attributeChangedCallback
 * 2. preFetch
 */
export function formatAppURL(url, appName = null) {
  if (!isString(url) || !url) return '';

  try {
    const { origin, pathname, search } = new URL(addProtocol(url));
    // If it ends with .html/.node/.php/.net/.etc, don’t need to add /
    if (/\.(\w+)$/.test(pathname)) {
      return `${origin}${pathname}${search}`;
    }
    const fullPath = `${origin}${pathname}/`.replace(/\/\/$/, '/');
    return /^https?:\/\//.test(fullPath) ? `${fullPath}${search}` : '';
  } catch (e) {
    logError(e, appName);
    return '';
  }
}

/**
 * format name
 * note the scenes:
 * 1. micro-app -> attributeChangedCallback
 * 2. event_center -> EventCenterForMicroApp -> constructor
 * 3. event_center -> EventCenterForBaseApp -> all methods
 * 4. preFetch
 * 5. plugins
 */
export function formatAppName(name) {
  if (!isString(name) || !name) return '';
  return name.replace(/(^\d+)|([^\w\d-_])/gi, '');
}

/**
 * Get valid address, such as https://xxx/xx/xx.html to https://xxx/xx/
 * @param url app.url
 */
export function getEffectivePath(url) {
  const { origin, pathname } = new URL(url);
  if (/\.(\w+)$/.test(pathname)) {
    const fullPath = `${origin}${pathname}`;
    const pathArr = fullPath.split('/');
    pathArr.pop();
    return pathArr.join('/') + '/';
  }

  return `${origin}${pathname}/`.replace(/\/\/$/, '/');
}

/**
 * Complete address
 * @param path address
 * @param baseURI base url(app.url)
 */
export function CompletionPath(path, baseURI) {
  if (!path || /^((((ht|f)tps?)|file):)?\/\//.test(path) || /^(data|blob):/.test(path)) return path;

  return new URL(path, getEffectivePath(addProtocol(baseURI))).toString();
}

/**
 * Get the folder where the link resource is located,
 * which is used to complete the relative address in the css
 * @param linkPath full link address
 */
export function getLinkFileDir(linkPath) {
  const pathArr = linkPath.split('/');
  pathArr.pop();
  return addProtocol(pathArr.join('/') + '/');
}

/**
 * promise stream
 * @param promiseList promise list
 * @param successCb success callback
 * @param errorCb failed callback
 * @param finallyCb finally callback
 */
export function promiseStream(promiseList, successCb, errorCb, finallyCb) {
  let finishedNum = 0;

  function isFinished() {
    if (++finishedNum === promiseList.length && finallyCb) finallyCb();
  }

  promiseList.forEach((p, i) => {
    if (isPromise(p)) {
      p.then((res) => {
        successCb({
          data: res,
          index: i,
        });
        isFinished();
      }).catch((err) => {
        errorCb({
          error: err,
          index: i,
        });
        isFinished();
      });
    } else {
      successCb({
        data: p,
        index: i,
      });
      isFinished();
    }
  });
}

// Check whether the browser supports module script
export function isSupportModuleScript() {
  const s = document.createElement('script');
  return 'noModule' in s;
}

// Create a random symbol string
export function createNonceSrc() {
  return 'inline-' + Math.random().toString(36).substr(2, 15);
}

// Array deduplication
export function unique(array) {
  return array.filter(function (item) {
    return item in this ? false : (this[item] = true);
  }, Object.create(null));
}

// requestIdleCallback polyfill
export const requestIdleCallback =
  globalThis.requestIdleCallback ||
  function (fn) {
    const lastTime = Date.now();
    return setTimeout(function () {
      fn({
        didTimeout: false,
        timeRemaining() {
          return Math.max(0, 50 - (Date.now() - lastTime));
        },
      });
    }, 50);
  };

/**
 * Record the currently running app.name
 */
let currentMicroAppName = null;
export function setCurrentAppName(appName) {
  currentMicroAppName = appName;
}

export function throttleDeferForSetAppName(appName) {
  if (currentMicroAppName !== appName) {
    setCurrentAppName(appName);
    defer(() => {
      setCurrentAppName(null);
    });
  }
}

// get the currently running app.name
export function getCurrentAppName() {
  return currentMicroAppName;
}

// Clear appName
export function removeDomScope() {
  setCurrentAppName(null);
}

// is safari browser
export function isSafari() {
  return /Safari/.test(navigator.userAgent) && !/Chrome/.test(navigator.userAgent);
}

/**
 * Create pure elements
 */
export function pureCreateElement(tagName, options) {
  const element = document.createElement(tagName, options);
  if (element.__MICRO_APP_NAME__) delete element.__MICRO_APP_NAME__;
  return element;
}

/**
 * clone origin elements to target
 * @param origin Cloned element
 * @param target Accept cloned elements
 * @param deep deep clone or transfer dom
 */
export function cloneContainer(origin, target, deep) {
  target.innerHTML = '';
  if (deep) {
    const clonedNode = origin.cloneNode(true);
    const fragment = document.createDocumentFragment();
    Array.from(clonedNode.childNodes).forEach((node) => {
      fragment.appendChild(node);
    });
    target.appendChild(fragment);
  } else {
    Array.from(origin.childNodes).forEach((node) => {
      target.appendChild(node);
    });
  }
}

// is invalid key of querySelector
export function isInvalidQuerySelectorKey(key) {
  if (window.__TEST__) return !key || /(^\d)|([^\w\d-_$])/gi.test(key);
  return !key || /(^\d)|([^\w\d-_\u4e00-\u9fa5])/gi.test(key);
}

// unique element
export function isUniqueElement(key) {
  return /^body$/i.test(key) || /^head$/i.test(key) || /^html$/i.test(key);
}

/**
 * get micro-app element
 * @param target app container
 */
export function getRootContainer(target) {
  return isShadowRoot(target) ? target.host : target;
}

/**
 * trim start & end
 */
export function trim(str) {
  return str ? str.replace(/^\s+|\s+$/g, '') : '';
}

export function isFireFox() {
  return navigator.userAgent.indexOf('Firefox') > -1;
}
